<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
  <title>Vue3 语音录制应用</title>
  <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
  <style>
    * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
      font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif;
      -webkit-tap-highlight-color: transparent;
    }
    
    body {
      background: linear-gradient(135deg, #6e8efb, #a777e3);
      color: #333;
      min-height: 100vh;
      padding: 20px;
      display: flex;
      justify-content: center;
      align-items: center;
    }
    
    #app {
      width: 100%;
      max-width: 500px;
      background: rgba(255, 255, 255, 0.95);
      border-radius: 20px;
      padding: 25px;
      box-shadow: 0 10px 30px rgba(0, 0, 0, 0.15);
    }
    
    header {
      text-align: center;
      margin-bottom: 30px;
    }
    
    h1 {
      color: #2c3e50;
      font-size: 28px;
      margin-bottom: 10px;
    }
    
    .subtitle {
      color: #7f8c8d;
      font-size: 16px;
    }
    
    .permission-panel {
      background: #f8f9fa;
      border-radius: 12px;
      padding: 20px;
      margin-bottom: 20px;
      text-align: center;
    }
    
    .permission-status {
      display: flex;
      align-items: center;
      justify-content: center;
      margin-bottom: 15px;
      font-weight: 500;
    }
    
    .status-dot {
      width: 12px;
      height: 12px;
      border-radius: 50%;
      margin-right: 10px;
    }
    
    .status-granted {
      background: #4CAF50;
    }
    
    .status-denied {
      background: #f44336;
    }
    
    .status-pending {
      background: #FF9800;
    }
    
    .permission-help {
      font-size: 14px;
      color: #7f8c8d;
      margin-top: 10px;
    }
    
    .recorder-container {
      display: flex;
      flex-direction: column;
      align-items: center;
      margin-bottom: 30px;
    }
    
    .record-button {
      width: 80px;
      height: 80px;
      border-radius: 50%;
      background: linear-gradient(135deg, #ff6b6b, #ff8e8e);
      border: none;
      color: white;
      font-size: 14px;
      cursor: pointer;
      display: flex;
      flex-direction: column;
      justify-content: center;
      align-items: center;
      box-shadow: 0 4px 15px rgba(255, 107, 107, 0.4);
      transition: all 0.3s;
      position: relative;
      overflow: hidden;
      touch-action: manipulation;
    }
    
    .record-button:active, .record-button.recording {
      transform: scale(0.95);
      box-shadow: 0 2px 8px rgba(255, 107, 107, 0.6);
    }
    
    .record-button::after {
      content: '';
      position: absolute;
      width: 100%;
      height: 100%;
      background: rgba(255, 255, 255, 0.2);
      border-radius: 50%;
      transform: scale(0);
      opacity: 0;
    }
    
    .record-button.recording::after {
      animation: pulse 1.5s infinite;
    }
    
    @keyframes pulse {
      0% {
        transform: scale(0.8);
        opacity: 0.7;
      }
      100% {
        transform: scale(2);
        opacity: 0;
      }
    }
    
    .record-button i {
      font-size: 24px;
      margin-bottom: 5px;
    }
    
    .timer {
      margin-top: 15px;
      font-size: 18px;
      font-weight: 600;
      color: #2c3e50;
    }
    
    .status {
      margin-top: 10px;
      font-size: 14px;
      color: #7f8c8d;
      text-align: center;
      min-height: 20px;
    }
    
    .recordings-container {
      margin-top: 20px;
    }
    
    .recordings-header {
      display: flex;
      justify-content: space-between;
      align-items: center;
      margin-bottom: 15px;
      padding-bottom: 10px;
      border-bottom: 1px solid #eee;
    }
    
    .recordings-title {
      font-size: 18px;
      font-weight: 600;
      color: #2c3e50;
    }
    
    .clear-button {
      background: none;
      border: none;
      color: #e74c3c;
      cursor: pointer;
      font-size: 14px;
    }
    
    .recordings-list {
      max-height: 300px;
      overflow-y: auto;
    }
    
    .recording-item {
      display: flex;
      align-items: center;
      padding: 12px 15px;
      background: #f8f9fa;
      border-radius: 12px;
      margin-bottom: 10px;
      transition: transform 0.2s;
    }
    
    .recording-item:hover {
      transform: translateY(-2px);
      box-shadow: 0 4px 8px rgba(0, 0, 0, 0.05);
    }
    
    .play-button {
      width: 36px;
      height: 36px;
      border-radius: 50%;
      background: #4CAF50;
      border: none;
      color: white;
      display: flex;
      justify-content: center;
      align-items: center;
      cursor: pointer;
      margin-right: 15px;
      flex-shrink: 0;
    }
    
    .recording-info {
      flex-grow: 1;
    }
    
    .recording-date {
      font-size: 12px;
      color: #7f8c8d;
      margin-bottom: 4px;
    }
    
    .recording-duration {
      font-size: 14px;
      color: #2c3e50;
      font-weight: 500;
    }
    
    .recording-actions {
      display: flex;
      gap: 10px;
    }
    
    .download-button, .delete-button {
      padding: 6px 12px;
      border-radius: 6px;
      border: none;
      cursor: pointer;
      font-size: 13px;
      display: flex;
      align-items: center;
      gap: 5px;
    }
    
    .download-button {
      background: #3498db;
      color: white;
    }
    
    .delete-button {
      background: #e74c3c;
      color: white;
    }
    
    .empty-state {
      text-align: center;
      padding: 30px 0;
      color: #7f8c8d;
    }
    
    .waveform {
      display: flex;
      align-items: center;
      height: 30px;
      margin-top: 5px;
    }
    
    .wave-bar {
      width: 3px;
      height: 100%;
      background: #3498db;
      margin: 0 1px;
      border-radius: 2px;
      transform-origin: bottom;
      animation: wave 1.2s ease-in-out infinite;
    }
    
    @keyframes wave {
      0%, 100% { transform: scaleY(0.4); }
      50% { transform: scaleY(1); }
    }
    
    .wave-bar:nth-child(2) { animation-delay: 0.1s; }
    .wave-bar:nth-child(3) { animation-delay: 0.2s; }
    .wave-bar:nth-child(4) { animation-delay: 0.3s; }
    .wave-bar:nth-child(5) { animation-delay: 0.4s; }
    
    .request-permission-btn {
      background: #3498db;
      color: white;
      border: none;
      padding: 10px 20px;
      border-radius: 8px;
      cursor: pointer;
      font-size: 16px;
      margin-top: 10px;
    }
    
    @media (max-width: 500px) {
      #app {
        padding: 15px;
      }
      
      h1 {
        font-size: 24px;
      }
      
      .record-button {
        width: 70px;
        height: 70px;
        font-size: 13px;
      }
    }
  </style>
</head>
<body>
  <div id="app">
    <header>
      <h1>语音录制器</h1>
      <p class="subtitle">长按录制，松开停止</p>
    </header>
    
    <main>
      <div class="permission-panel" v-if="showPermissionPanel">
        <div class="permission-status">
          <div class="status-dot" :class="permissionStatusClass"></div>
          {{ permissionStatusText }}
        </div>
        <p class="permission-help" v-if="permissionStatus === 'denied'">
          请在浏览器设置中允许麦克风权限，然后刷新页面
        </p>
        <p class="permission-help" v-else-if="permissionStatus === 'prompt'">
          请点击下方按钮授权麦克风访问权限
        </p>
        <button 
          class="request-permission-btn" 
          @click="requestPermission"
          v-if="permissionStatus === 'prompt' || permissionStatus === 'denied'"
        >
          授权麦克风权限
        </button>
      </div>
      
      <div class="recorder-container">
        <button 
          class="record-button" 
          @mousedown="startRecording" 
          @mouseup="stopRecording"
          @touchstart="onTouchStart" 
          @touchend="onTouchEnd"
          :class="{ recording: isRecording }"
          :disabled="!isPermissionGranted"
        >
          <i>{{ isRecording ? '⏹️' : '🎤' }}</i>
          {{ isRecording ? '松开停止' : '按住录音' }}
        </button>
        
        <div class="timer" v-if="isRecording || recordedTime">
          {{ formatTime(currentTime) }}
        </div>
        
        <div class="status">
          {{ statusMessage }}
        </div>
      </div>
      
      <div class="recordings-container">
        <div class="recordings-header">
          <h2 class="recordings-title">录音列表</h2>
          <button class="clear-button" @click="clearRecordings" v-if="recordings.length">清空列表</button>
        </div>
        
        <div class="recordings-list">
          <div class="empty-state" v-if="recordings.length === 0">
            暂无录音，请长按上方按钮开始录制
          </div>
          
          <div v-for="(recording, index) in recordings" :key="index" class="recording-item">
            <button class="play-button" @click="togglePlay(recording)">
              {{ recording.playing ? '⏸️' : '▶️' }}
            </button>
            
            <div class="recording-info">
              <div class="recording-date">{{ recording.date }}</div>
              <div class="recording-duration">时长: {{ formatTime(recording.duration) }}</div>
              <div class="waveform" v-if="recording.playing">
                <div class="wave-bar"></div>
                <div class="wave-bar"></div>
                <div class="wave-bar"></div>
                <div class="wave-bar"></div>
                <div class="wave-bar"></div>
              </div>
            </div>
            
            <div class="recording-actions">
              <button class="download-button" @click="downloadRecording(recording)">
                <span>下载</span>
              </button>
              <button class="delete-button" @click="deleteRecording(index)">
                <span>删除</span>
              </button>
            </div>
          </div>
        </div>
      </div>
    </main>
  </div>

  <script>
    const { createApp, ref, onMounted, computed } = Vue;
    
    createApp({
      setup() {
        const isRecording = ref(false);
        const isRecordingSupported = ref(true);
        const statusMessage = ref('准备就绪');
        const currentTime = ref(0);
        const recordedTime = ref(0);
        const recordings = ref([]);
        const timerInterval = ref(null);
        const mediaRecorder = ref(null);
        const audioChunks = ref([]);
        const permissionStatus = ref('prompt'); // 'prompt', 'granted', 'denied'
        const showPermissionPanel = ref(true);
        
        const permissionStatusText = computed(() => {
          switch(permissionStatus.value) {
            case 'granted': return '麦克风权限已授权';
            case 'denied': return '麦克风权限被拒绝';
            default: return '需要麦克风权限';
          }
        });
        
        const permissionStatusClass = computed(() => {
          switch(permissionStatus.value) {
            case 'granted': return 'status-granted';
            case 'denied': return 'status-denied';
            default: return 'status-pending';
          }
        });
        
        const isPermissionGranted = computed(() => {
          return permissionStatus.value === 'granted';
        });
        
        // 检查浏览器支持
        onMounted(() => {
          if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
            isRecordingSupported.value = false;
            statusMessage.value = '您的浏览器不支持录音功能';
            permissionStatus.value = 'denied';
            return;
          }
          
          // 检查之前的权限状态
          navigator.mediaDevices.enumerateDevices()
            .then(devices => {
              const hasMicrophone = devices.some(device => device.kind === 'audioinput');
              if (!hasMicrophone) {
                statusMessage.value = '未检测到麦克风设备';
                permissionStatus.value = 'denied';
                return;
              }
              
              // 尝试获取当前权限状态
              navigator.mediaDevices.getUserMedia({ audio: true })
                .then(stream => {
                  permissionStatus.value = 'granted';
                  showPermissionPanel.value = false;
                  // 立即关闭流，我们只需要检查权限
                  stream.getTracks().forEach(track => track.stop());
                })
                .catch(err => {
                  if (err.name === 'NotAllowedError' || err.name === 'PermissionDeniedError') {
                    permissionStatus.value = 'denied';
                    statusMessage.value = '麦克风权限已被拒绝';
                  } else {
                    permissionStatus.value = 'prompt';
                    statusMessage.value = '请授权麦克风权限';
                  }
                });
            })
            .catch(err => {
              console.error('Error enumerating devices:', err);
              permissionStatus.value = 'prompt';
            });
        });
        
        // 请求麦克风权限
        const requestPermission = async () => {
          try {
            statusMessage.value = '正在请求麦克风权限...';
            const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
            permissionStatus.value = 'granted';
            showPermissionPanel.value = false;
            statusMessage.value = '权限已授权，可以开始录音';
            // 立即关闭流，我们只需要权限
            stream.getTracks().forEach(track => track.stop());
          } catch (error) {
            console.error('Error requesting permission:', error);
            if (error.name === 'NotAllowedError' || error.name === 'PermissionDeniedError') {
              permissionStatus.value = 'denied';
              statusMessage.value = '麦克风权限请求被拒绝';
            } else {
              statusMessage.value = '无法访问麦克风：' + error.message;
            }
          }
        };
        
        // 开始录音
        const startRecording = async () => {
          if (!isPermissionGranted.value) {
            statusMessage.value = '请先授权麦克风权限';
            showPermissionPanel.value = true;
            return;
          }
          
          try {
            statusMessage.value = '正在启动录音...';
            const stream = await navigator.mediaDevices.getUserMedia({ 
              audio: {
                echoCancellation: true,
                noiseSuppression: true
              }
            });
            
            audioChunks.value = [];
            mediaRecorder.value = new MediaRecorder(stream);
            
            mediaRecorder.value.ondataavailable = (event) => {
              audioChunks.value.push(event.data);
            };
            
            mediaRecorder.value.onstop = () => {
              const audioBlob = new Blob(audioChunks.value, { type: 'audio/webm' });
              const audioUrl = URL.createObjectURL(audioBlob);
              
              // 添加到录音列表
              recordings.value.unshift({
                audioUrl: audioUrl,
                duration: currentTime.value,
                date: new Date().toLocaleString(),
                playing: false,
                audio: new Audio(audioUrl)
              });
              
              // 释放stream
              stream.getTracks().forEach(track => track.stop());
            };
            
            mediaRecorder.value.start();
            isRecording.value = true;
            statusMessage.value = '正在录音...';
            
            // 开始计时
            currentTime.value = 0;
            timerInterval.value = setInterval(() => {
              currentTime.value += 0.1;
            }, 100);
            
          } catch (error) {
            console.error('Error accessing microphone:', error);
            statusMessage.value = '无法访问麦克风：' + error.message;
            if (error.name === 'NotAllowedError') {
              permissionStatus.value = 'denied';
              showPermissionPanel.value = true;
            }
          }
        };
        
        // 触摸开始事件处理
        const onTouchStart = (e) => {
          e.preventDefault();
          startRecording();
        };
        
        // 触摸结束事件处理
        const onTouchEnd = (e) => {
          e.preventDefault();
          stopRecording();
        };
        
        // 停止录音
        const stopRecording = () => {
          if (!isRecording.value) return;
          
          if (mediaRecorder.value && mediaRecorder.value.state !== 'inactive') {
            mediaRecorder.value.stop();
          }
          
          isRecording.value = false;
          recordedTime.value = currentTime.value;
          statusMessage.value = '录音完成';
          
          // 停止计时
          clearInterval(timerInterval.value);
        };
        
        // 切换播放/暂停
        const togglePlay = (recording) => {
          if (recording.playing) {
            recording.audio.pause();
            recording.playing = false;
          } else {
            // 先暂停所有其他录音
            recordings.value.forEach(r => {
              if (r.playing && r !== recording) {
                r.audio.pause();
                r.playing = false;
              }
            });
            
            recording.audio.play();
            recording.playing = true;
            
            recording.audio.onended = () => {
              recording.playing = false;
            };
          }
        };
        
        // 下载录音
        const downloadRecording = (recording) => {
          const a = document.createElement('a');
          a.href = recording.audioUrl;
          a.download = `recording-${recording.date.replace(/[/:\\]/g, '-')}.webm`;
          document.body.appendChild(a);
          a.click();
          document.body.removeChild(a);
        };
        
        // 删除录音
        const deleteRecording = (index) => {
          // 释放音频对象URL
          URL.revokeObjectURL(recordings.value[index].audioUrl);
          recordings.value.splice(index, 1);
        };
        
        // 清空所有录音
        const clearRecordings = () => {
          // 释放所有音频对象URL
          recordings.value.forEach(recording => {
            URL.revokeObjectURL(recording.audioUrl);
          });
          recordings.value = [];
        };
        
        // 格式化时间
        const formatTime = (time) => {
          const minutes = Math.floor(time / 60);
          const seconds = Math.floor(time % 60);
          const milliseconds = Math.floor((time % 1) * 100);
          
          return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}.${milliseconds.toString().padStart(2, '0')}`;
        };
        
        return {
          isRecording,
          isRecordingSupported,
          statusMessage,
          currentTime,
          recordedTime,
          recordings,
          permissionStatus,
          permissionStatusText,
          permissionStatusClass,
          isPermissionGranted,
          showPermissionPanel,
          startRecording,
          stopRecording,
          onTouchStart,
          onTouchEnd,
          togglePlay,
          downloadRecording,
          deleteRecording,
          clearRecordings,
          requestPermission,
          formatTime
        };
      }
    }).mount('#app');
  </script>
</body>
</html>